/*
 * Copyright (c) 2021-2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package escompat;

// NOTE: autogenerated file

% template = ERB.new(File.read("Array_header.erb"), trim_mode: '%', eoutvar: '_sub04')
<%= template.result(binding) %>

class ArrayKeysIterator<T> implements IterableIterator<number> {
    private parent: Array<T>
    private idx: int = 0
    private isDone: boolean = false

    constructor(parent: Array<T>) {
        this.parent = parent
    }

    override next(): IteratorResult<number> {
        if (this.isDone || this.idx >= this.parent.actualLength) {
            this.isDone = true
            return new IteratorResult<number>()
        }
        return new IteratorResult<number>(this.idx++ as number)
    }

    override $_iterator(): IterableIterator<number> {
        return this
    }
}

class FromBuffer {}
// initialized in _initializerBlock_.ets
const FROM_BUFFER: FromBuffer;

/**
 * Represents JS API-compatible Array
 */
export class Array<T> implements ReadonlyArray<T>, Iterable<T> {
    private buffer: FixedArray<NullishType>
    internal actualLength: int

    override get length(): number {
        return this.actualLength as number
    }

    set length(newLen: number) {
        const len = newLen as int
        if (len < 0 || len > this.actualLength) {
            throw new RangeError("can't change length to bigger or negative")
        }
        this.actualLength = len
    }

    public override $_get(index: number): T {
        return this.$_get(index as int)
    }

    public $_set(i: number, val: T): void {
        this.$_set(i as int, val)
    }

    public native $_get(idx: int): T;

    internal final $_get_unsafe(idx: int): T {
        return this.buffer[idx] as T
    }

    public native $_set(idx: int, val: T): void;

    private $_set_unsafe(idx: int, val: T): void {
        this.buffer[idx] = val
    }

    /**
     * Creates a new instance of Array
     */
    public constructor(arrayLen: int) {
        this.buffer = new NullishType[arrayLen]
        this.actualLength = arrayLen
    }

    public constructor(arrayLen: number) {
        this(arrayLen as int)
    }

    internal constructor(_tag: FromBuffer, buf: FixedArray<NullishType>) {
        this.buffer = buf
        this.actualLength = buf.length
    }

    internal constructor() {
        this.buffer = new NullishType[4]
        this.actualLength = 0
    }

    /**
     * Creates a new instance of Array based on Object[]
     *
     * @param d Array initializer
     */
    public constructor(first: T, ...d: T[]) {
        this.buffer = new NullishType[d.length + 1]
        this.actualLength = d.length as int + 1

        this.buffer[0] = first

        for (let k: int = 0; k < d.length; k++) {
            this.$_set_unsafe(k + 1, d[k])
        }
    }

    /**
     * Creates a new instance of Array.
     *
     * @param arrayLength amount of elements.
     *
     * @param initialValue initial value of elements.
     *
     */
    public static create<T>(arrayLength: number, initialValue: T): Array<T> {
        let other = new Array<T>(arrayLength as int)
        other.fill(initialValue)
        return other
    }

    /**
     * Extends Array with new elements to specified length.
     *
     * @param arrayLength new length of the array.
     *
     * @param initialValue initial value of added elements.
     *
     */
    public extendTo(arrayLength: number, initialValue: T): void {
        if (arrayLength > Int.MAX_VALUE) {
            throw new RangeError("arrayLength must be <= int32 max")
        }
        const len = arrayLength as int
        const delta: int = len - this.actualLength
        if (delta <= 0) {
            return
        }
        this.ensureUnusedCapacity(delta)
        for (let i: int = 0; i < delta; i++) {
            this.buffer[this.actualLength + i] = initialValue
        }
        this.actualLength = len
    }

    /**
     * Shrinks Array to specified length.
     *
     * @param arrayLength length at which to shrink.
     *
     */
    public shrinkTo(arrayLength: number): void {
        if (arrayLength >= this.actualLength) {
            return
        }
        let newLen: int = arrayLength as int
        if (newLen < 0) {
            // Convert from signed to unsigned
            newLen = newLen & Int.MAX_VALUE
        }
        const other = this.slice(0, newLen)
        this.buffer = other.buffer
        this.actualLength = other.actualLength
    }

    /**
     * Creates a new instance of an Array with the specified length
     *
     * @param arrayLength The length of the array to be created (optional).
     *
     * @returns A new Array instance with the specified length
     */
    static $_invoke<T>(): Array<T> {
        return new Array<T>();
    }
    
    /**
     * Creates a new instance of an Array with the specified length
     *
     * @param arrayLength The length of the array to be created (optional).
     *
     * @returns A new Array instance with the specified length
     */
    static $_invoke<T>(arrayLength?: number): Array<T> {
        if (arrayLength != undefined) {
            return new Array<T>(arrayLength);
        } else {
            return new Array<T>();
        }
    }

    /**
     * Creates a new instance of an Array with the specified length
     *
     * @param items array of values.
     *
     * @returns A new Array instance with the specified length
     */
    static $_invoke<T>(...items: T[]): Array<T> {
        if (items.length == 0) {
            return new Array<T>(0)
        }
        return new Array<T>(items[0], ...items.slice(1))
    }

    /**
     * Creates a new `Array` instance from `Object[]` primitive array.
     *
     * @param iterable an iterable object to convert to an array.
     *
     * @returns `Array` intance constructed from `Object[]` primitive array.
     */
    public static from<T>(iterable: ArrayLike<T> | Iterable<T>): Array<T> {
        const ret = new Array<T>()
        iteratorForEach<T>(iterable.$_iterator(), (x: T): void => {
            ret.push(x)
        })
        return ret
    }

    /**
     * Creates a new `Array` instance from `Object[]` ArrayLike.
     *
     * @param arr an iterable object to convert to an array.
     *
     * @returns `Array` intance constructed from `Object[]` ArrayLike.
     */
    public static from<T>(arr: ArrayLike<T>): Array<T> {
        const ret = new Array<T>(arr.length as int)
        let i = 0
        iteratorForEach<T>(arr.$_iterator(), (x: T): void => {
            ret[i] = x
            i += 1
        })
        return ret
    }

    /**
     * Creates a new `Array` instance from `Object[]` primitive array.
     *
     * @param iterable an iterable object to convert to an array.
     *
     * @param mapfn a mapping function to call on every element of the array.
     * Every value to be added to the array is first passed through this function, and `mapfn`'s return value
     * is added to the array instead.
     *
     * @returns `Array` intance constructed from `Object[]` primitive array and given function.
     */
    public static from<T, U>(iterable: ArrayLike<T> | Iterable<T>, mapfn: (v: T, k: number) => U): Array<U> {
        const ret = new Array<U>()
        // NOTE (ikorobkov): Please don't replace idx as int[1] with int-variable, because of value of single variable doesn't change (idx++) into lambda call by unknown reason
        const idx : FixedArray<int> = new int[1]
        idx[0] = 0
        iteratorForEach<T>(iterable.$_iterator(), (x: T): void => {
            ret.push(mapfn(x, idx[0] as number))
            idx[0] += 1
        })
        return ret
    }

    public static from<T, U>(values: FixedArray<T>, mapfn: (v: T, k: number) => U): Array<U> {
        const ret = new Array<U>(values.length)
        for (let i = 0; i < values.length; ++i) {
            ret[i] = mapfn(values[i], i)
        }
        return ret
    }

    /**
    * Creates a new `Array` instance from `Object[]` primitive array.
    *
    * @param arr primitive array.
    *
    * @returns `Array` intance constructed from `Object[]` primitive array.
    */
    public static from<T>(arr: FixedArray<T>): Array<T> {
        const len = arr.length
        const ret : FixedArray<NullishType> = new NullishType[len as int]
        for (let i: int = 0; i < len; i++) {
            ret[i] = arr[i] as NullishType
        }
        return new Array<T>(FROM_BUFFER, ret)
    }

    private lastIndexOfUndefined(fi: int): int {
        for (let i = fi; i >= 0; i--) {
            if (this.$_get_unsafe(i) instanceof undefined) {
                return i
            }
        }
        return -1
    }

    private lastIndexOfNull(fi: int): int {
        for (let i = fi; i >= 0; i--) {
            if (this.$_get_unsafe(i) instanceof null) {
                return i
            }
        }
        return -1
    }

    private lastIndexOfString(val: String, fi: int): int {
        for (let i = fi; i >= 0; i--) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof String) {
                if (tmp == val) {
                    return i
                }
            }
        }
        return -1
    }

    private lastIndexOfNumber(val: Number, fi: int): int {
        const unboxedVal: number = val.valueOf()
        if (isNaN(val)) {
            return -1
        }
        for (let i = fi; i >= 0; i--) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Number) {
                if (unboxedVal == tmp.unboxed()) {
                    return i
                }
            }
        }
        return -1
    }

    private lastIndexOfFloat(val: Float, fi: int): int {
        const unboxedVal: float = val.unboxed()
        if (isNaN(val)) {
            return -1
        }
        for (let i = fi; i >= 0; i--) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Float) {
                if (unboxedVal == tmp.unboxed()) {
                        return i
                }
            }
        }
        return -1
    }

    private lastIndexOfLong(val: Long, fi: int): int {
        const unboxedVal: long = val.unboxed()
        for (let i = fi; i >= 0; i--) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Long) {
                if (tmp == unboxedVal) {
                    return i
                }
            }
        }
        return -1
    }

    private lastIndexOfInt(val: Int, fi: int): int {
        const unboxedVal: int = val.unboxed()
        for (let i = fi; i >= 0; i--) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Int) {
                if (tmp == unboxedVal) {
                    return i
                }
            }
        }
        return -1
    }

    private lastIndexOfCommon(val: T, fi: int): int {
        for (let i = fi; i >= 0; i--) {
            if (val == this.$_get_unsafe(i)) {
                return i
            }
        }
        return -1
    }

    private searchUndefined(fi: int, len: int): boolean {
        for (let i = fi; i < len; i++) {
            if (this.$_get_unsafe(i) instanceof undefined) {
                return true
            }
        }
        return false
    }

    private searchNull(fi: int, len: int): boolean {
        for (let i = fi; i < len; i++) {
            if (this.$_get_unsafe(i) instanceof null) {
                return true
            }
        }
        return false
    }

    private searchString(val: String, fi: int, len: int): boolean {
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof String) {
                if (tmp == val) {
                    return true
                }
            }
        }
        return false
    }

    private searchNumber(val: Number, fi: int, len: int): boolean {
        const unboxedVal: number = val.valueOf()
        if (isNaN(unboxedVal)) {
            for (let i = fi; i < len; i++) {
                const tmp = this.$_get_unsafe(i)
                if (tmp instanceof Number) {
                    if (isNaN(tmp.valueOf())) {
                        return true
                    }
                }
            }
        } else {
            for (let i = fi; i < len; i++) {
                const tmp = this.$_get_unsafe(i)
                if (tmp instanceof Number) {
                    if (unboxedVal == tmp.unboxed()) {
                        return true
                    }
                }
            }
        }
        return false
    }

    private searchFloat(val: Float, fi: int, len: int): boolean {
        const unboxedVal: float = val.unboxed()
        if (isNaN(unboxedVal)) {
            for (let i = fi; i < len; i++) {
                const tmp = this.$_get_unsafe(i)
                if (tmp instanceof Float) {
                    if (isNaN(tmp.unboxed())) {
                        return true
                    }
                }
            }
        } else {
            for (let i = fi; i < len; i++) {
                const tmp = this.$_get_unsafe(i)
                if (tmp instanceof Float) {
                    if (unboxedVal == tmp.unboxed()) {
                        return true
                    }
                }
            }
        }
        return false
    }

    private searchLong(val: Long, fi: int, len: int): boolean {
        const unboxedVal: long = val.unboxed()
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Long) {
                if (tmp == unboxedVal) {
                    return true
                }
            }
        }
        return false
    }

    private searchInt(val: Int, fi: int, len: int): boolean {
        const unboxedVal: int = val.unboxed()
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Int) {
                if (tmp == unboxedVal) {
                    return true
                }
            }
        }
        return false
    }

    private searchCommon(val: T, fi: int, len: int): boolean {
        for (let i = fi; i < len; i++) {
            if (val == this.$_get_unsafe(i)) {
                return true;
            }
        }
        return false
    }

    private indexOfUndefined(fi: int, len: int): int {
        for (let i = fi; i < len; i++) {
            if (this.$_get_unsafe(i) instanceof undefined) {
                return i
            }
        }
        return -1
    }

    private indexOfNull(fi: int, len: int): int {
        for (let i = fi; i < len; i++) {
            if (this.$_get_unsafe(i) instanceof null) {
                return i
            }
        }
        return -1
    }

    private indexOfString(val: String, fi: int, len: int): int {
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof String) {
                if (tmp == val) {
                    return i
                }
            }
        }
        return -1
    }

    private indexOfNumber(val: Number, fi: int, len: int): int {
        const unboxedVal: number = val.valueOf()
        if (isNaN(val)) {
            return -1
        }
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Number) {
                if (unboxedVal == tmp.unboxed()) {
                    return i
                }
            }
        }
        return -1
    }

    private indexOfFloat(val: Float, fi: int, len: int): int {
        const unboxedVal: float = val.unboxed()
        if (isNaN(val)) {
            return -1
        }
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Float) {
                if (unboxedVal == tmp.unboxed()) {
                    return i
                }
            }
        }
        return -1
    }

    private indexOfLong(val: Long, fi: int, len: int): int {
        const unboxedVal: long = val.unboxed()
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Long) {
                if (tmp == unboxedVal) {
                    return i
                }
            }
        }
        return -1
    }

    private indexOfInt(val: Int, fi: int, len: int): int {
        const unboxedVal: int = val.unboxed()
        for (let i = fi; i < len; i++) {
            const tmp = this.$_get_unsafe(i)
            if (tmp instanceof Int) {
                if (tmp == unboxedVal) {
                    return i
                }
            }
        }
        return -1
    }

    private indexOfCommon(val: T, fi: int, len: int): int {
        for (let i = fi; i < len; i++) {
            if (val == this.$_get_unsafe(i)) {
                return i
            }
        }
        return -1
    }
    /**
     * Default comparison function for sort algorithm.
     * Objects are compared as string. Both objects are convereted to string
     * using `toString()` method and compared using `compareTo() method of `string` class.
     *
     * @param a: Object - Object to be compared
     *
     * @param b: Object - Object to be compared
     *
     * @returns Returns one of values -1, 0, 1 (_less_, _equal_, _greater_ respectively).
     */
    private static defaultComparator(a: NullishType, b: NullishType): number {
        if (a instanceof Number && b instanceof Number) {
            const x = (a as Number).valueOf()
            const y = (b as Number).valueOf()
            if (Number.isInteger(x) && Number.isInteger(y) &&
                x <= Int.MAX_VALUE / 128 && x >= Int.MIN_VALUE / 128 &&
                y <= Int.MAX_VALUE / 128 && y >= Int.MIN_VALUE / 128) {
                let z = x as int
                let w = y as int
                return Array.defaultComparatorInts(z, w)
            }
        } else if (a instanceof String && b instanceof String) {
            return a.compareTo(b)
        }
        let sa = new String(a)
        let sb = new String(b)
        return sa.compareTo(sb)
    }

    private static defaultComparatorInts(a: int, b: int): number {
        if (a < 0) {
            if (b >= 0) {
                return -1
            }
            a *= -1
            b *= -1
        } else if (b < 0) {
            return 1
        }
        let aDigs = 1
        while (10 * aDigs <= a) {
            aDigs *= 10
        }
        let bDigs = 1
        while (10 * bDigs <= b) {
            bDigs *= 10
        }

        while (aDigs > 0 && bDigs > 0) {
            let r = (a / aDigs) - (b / bDigs)
            if (r != 0) {
                return r
            }
            aDigs /= 10
            bDigs /= 10
        }
        return (aDigs - bDigs)
    }

    private static defaultComparatorStr(a: String, b: String) {
        return a.compareTo(b)
    }

    /**
     * Helper function preparing copy of `this` instance of `Array` class' data array.
     *
     * @returns Copy of an `Array`'s primitive array data.
     */
    private copyArray(): FixedArray<NullishType> {
        let len: int = this.actualLength
        let res : FixedArray<NullishType> = new NullishType[len]
        for (let i = 0; i < len; i++) {
            res[i] = this.$_get_unsafe(i)
        }
        return res
    }

    private wrap_default_sort(): void {
        let idxNonUndef = 0
        type arrType = String | undefined
        try {
            let strArr : FixedArray<arrType> = new arrType[this.actualLength]
            for (let i = 0; i < this.actualLength; i++) {
                const vl = this.$_get_unsafe(i)
                if (vl !== undefined) {
                    if (vl == null) {
                        this.$_set_unsafe(idxNonUndef, vl as T)
                        strArr[idxNonUndef] = "null"
                    } else {
                        this.$_set_unsafe(idxNonUndef, vl)
                        strArr[idxNonUndef] = (vl as object).toString() // #26217
                    }
                    idxNonUndef++
                }
            }
            let sortTo = idxNonUndef
            for (let i = idxNonUndef; i < this.actualLength; i++) {
                this.$_set_unsafe(i, undefined as T)
            }

            sort_default<NullishType>(this.buffer, strArr, 0, sortTo)
        }
        catch (e) {
            if (e instanceof OutOfMemoryError) {
                this.slow_default_sort()
            } else {
                throw e as Error
            }
        }
    }

    private slow_default_sort(): void {
        let idxNonUndef = 0
        const cmp: (l: NullishType, r: NullishType) => number = (l: NullishType, r: NullishType): number => {
            return Array.defaultComparator(l, r)
        }
        for (let i = 0; i < this.actualLength; i++) {
            const vl = this.$_get_unsafe(i)
            if (vl !== undefined) {
                this.$_set_unsafe(idxNonUndef, vl)
                idxNonUndef++
            }
        }
        let sortTo = idxNonUndef
        for (let i = idxNonUndef; i < this.actualLength; i++) {
            this.$_set_unsafe(i, undefined as T)
        }
        sort_stable<NullishType>(this.buffer, 0, sortTo, cmp)
    }

    private move_undefined_end(): int {
        let writeIndex: int = 0
        for (let i = 0; i < this.actualLength; i++) {
            let val = this.$_get_unsafe(i)
            if (val !== undefined) {
                if(writeIndex != i) {
                    this.$_set_unsafe(writeIndex, val)
                }
                writeIndex++
            } 
        }
        for (let i = writeIndex; i < this.actualLength; i++) {
            this.$_set_unsafe(i, undefined as T)
        }
        return writeIndex
    }

    /**
     * Reorders elements of `this` using comparator function.
     *
     * @see ECMA-262, 23.1.3.30
     *
     * @param comparator function that defines the sort order.
     *
     * @returns `this` instance of `Array` class.
     *
     * @note Mutating method
     *
     * NOTE clarify UTF-16 or UTF-8
     */
    public sort(comparator?: (a: T, b: T) => number): this {
        if (this.actualLength <= 1)
            return this

        if (comparator == undefined) {
            this.wrap_default_sort()
            return this
        }

        const compareTo = this.move_undefined_end()
        let cmp: (l: NullishType, r: NullishType) => number = (l: NullishType, r: NullishType): number => {
            return comparator!(l as T, r as T)
        }
        sort_stable<NullishType>(this.buffer, 0, compareTo, cmp)
        return this
    }

    /**
     * Removes the first element from an array and returns that removed element.
     * This method changes the length of the array.
     *
     * @returns shifted element, i.e. that was at index zero
     */
    public shift(): T | undefined {
        if(this.actualLength == 0) {
            return undefined
        }
        let obj = this.$_get_unsafe(0)
        const other = this.slice(1, this.actualLength)
        this.buffer = other.buffer
        this.actualLength = other.actualLength
        return obj
    }

    /**
     * Removes the last element from an array and returns that element.
     * This method changes the length of the array.
     *
     * @returns removed element
     */
    public pop(): T | undefined {
        if(this.actualLength == 0) {
            return undefined
        }
        let obj = this.$_get_unsafe(this.actualLength - 1)
        this.buffer[this.actualLength - 1] = null
        this.actualLength--
        return obj
    }

    /**
     * Adds the specified elements to the end of an array and returns the new length of the array.
     *
     * @returns new length
     */
    public push(...val: T[]): number {
        this.ensureUnusedCapacity(val.length as int)
        for (let i = 0; i < val.length; i++) {
            this.buffer[this.actualLength + i] = val[i]
        }
        this.actualLength += val.length
        return this.actualLength
    }

    /**
     * Adds the specified elements to the end of an array and returns the new length of the array.
     *
     * @returns new length
     */
    public pushECMA(...val: T[]): number {
        this.ensureUnusedCapacity(val.length as int)
        for (let i = 0; i < val.length; i++) {
            this.buffer[this.actualLength + i] = val[i]
        }
        this.actualLength += val.length
        return this.actualLength
    }

    private ensureUnusedCapacity(cap: int): void {
        if (this.actualLength + cap > this.buffer.length) {
            const copy : FixedArray<NullishType> = new NullishType[this.buffer.length * 2 + cap]
            for (let i = 0; i < this.actualLength; i++) {
                copy[i] = this.buffer[i]
            }
            this.buffer = copy
        }
    }

    /**
     * Changes the contents of an array by removing or replacing existing elements
     * and/or adding new elements in place.
     *
     * @param start index
     *
     * @param delete number of items after start index
     *
     * @returns an Array with deleted elements
     */
    public splice(start: number, delete: Number | undefined, ...items: T[]): Array<T> {
        return this.splice(start as int, asIntOrDefault(delete, 0), ...items)
    }

    /**
     * Changes the contents of an array by removing or replacing existing elements
     * and/or adding new elements in place.
     *
     * @param start index
     *
     * @param delete number of items after start index
     *
     * @returns an Array with deleted elements
     */
    public splice(start: int, delete: int, ...items: T[]): Array<T> {
        start = normalizeIndex(start, this.actualLength)
        if (delete < 0) {
            delete = 0
        }
        if (start > this.actualLength - delete) {
            delete = this.actualLength - start
        }
        // this: [left middle right], we must replace middle with `items`

        this.ensureUnusedCapacity(items.length as int - delete)
        const oldLen = this.actualLength
        this.actualLength = this.actualLength - delete + items.length as int

        let ret = new Array<T>(delete)
        let lastSet = start
        // left part remains unchanged
        // copy excluded part
        for (let i = 0; i < delete; i++) {
            ret.buffer[i] = this.buffer[start + i]
        }
        // move right part to the right of the buffer
        const rightLen = oldLen - start - delete
        if (items.length > delete) {
            for (let i = 0; i < rightLen; i++) {
                this.buffer[this.actualLength - 1 - i] = this.buffer[oldLen - 1 - i]
            }
        } else {
            for (let i = 0; i < rightLen; i++) {
                this.buffer[start + items.length as int + i] = this.buffer[start + delete + i]
            }
        }
        // insert middle part
        for (let i = 0; i < items.length; i++) {
            this.buffer[start + i] = items[i]
        }
        return ret
    }

    /**
     * Changes the contents of an array by removing or replacing existing elements
     * and/or adding new elements in place.
     *
     * @param start index
     *
     * @returns an Array with deleted elements from start to the last element of the current instance
     */
    public splice(start: number): Array<T> {
        return this.splice(start as int)
    }

    /**
     * Changes the contents of an array by removing or replacing existing elements
     * and/or adding new elements in place.
     *
     * @param start index
     *
     * @returns an Array with deleted elements from start to the last element of the current instance
     */
    public splice(start: int): Array<T> {
        return this.splice(start, this.actualLength)
    }

    /**
     * Checks whether the passed value is an Array.
     *
     * @param arr
     *
     * @returns true is arr is a non-nullish array, false otherwise
     */
    public static isArray(o: NullishType): boolean {
        if (o instanceof Array) {
            return true
        }
        return (Type.of(o) instanceof ArrayType)
    }

    /**
     * Creates a new Array instance from a variable number of arguments,
     * regardless of number or type of the arguments.
     *
     * @param values an initilizer
     *
     * @returns a newly created Array
     */
    public static of<T>(...values: T[]): Array<T> {
        const ret = new Array<T>()
        ret.ensureUnusedCapacity(values.length as int)
        for (let i = 0; i < values.length; i++) {
            ret.push(values[i])
        }
        return ret
    }

    /**
     * Adds the specified elements to the beginning of an Array
     * and returns the new length of the Array.
     *
     * @param values data to be added
     *
     * @returns new length of the Array
     */
    public unshift(...values: T[]): number {
        let buffer = this.buffer
        if (this.buffer.length <= values.length + this.actualLength) {
            buffer = new NullishType[this.buffer.length * 2 + values.length]
        }
        for (let i = 0; i < this.actualLength; i++) {
            buffer[this.actualLength + values.length as int - i - 1] = this.buffer[this.actualLength - 1 - i]
        }
        for (let i = 0; i < values.length; i++) {
            buffer[i] = values[i]
        }
        this.buffer = buffer
        this.actualLength += values.length
        return this.actualLength
    }

    /**
     * Returns an iterator over all indices
     */
    public override keys(): IterableIterator<Number> {
        return new ArrayKeysIterator<T>(this)
    }

    /**
     * Returns an iterator over all values
     */
    public override $_iterator(): IterableIterator<T> {
        return this.values()
    }

    // === methods with uncompatible implementation ===
    /**
     * Returns the elements of an array that meet the condition specified in a callback function.
     *
     * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
     *
     * @returns New `Array` instance constructed from `this` with elements filtered using test function `predicate`.
     */
    public override filter(predicate: (value: T, index: number, array: Array<T>) => boolean): Array<T> {
        const res = new Array<T>()
        for (let i: int = 0; i < this.actualLength; i++) {
            const val = this.$_get_unsafe(i)
            if (predicate(val, i as number, this)) {
                res.push(val)
            }
        }
        return res
    }

    /**
     * Creates a new Array with all sub-array elements concatenated
     * into it recursively up to the specified depth.
     *
     * @param depth
     *
     * @returns a flattened Array with respect to depth
     */
    public flat<U>(depth: number): Array<U> {
        return this.flat<U>(depth as int)
    }

    /**
     * Creates a new Array with all sub-array elements concatenated
     * into it recursively up to the specified depth.
     *
     * @param depth
     *
     * @returns a flattened Array with respect to depth
     */
    public flat<U>(depth: int): Array<U> {
        let ret = new Array<U>()
        this.flatImpl<U>(depth, ret)
        return ret
    }

    private flatImpl<U>(depth: int, to: Array<U>) {
        throw new Error("not implemented")
    }

    /**
     * Creates a new Array with all sub-array elements concatenated
     *
     * @returns a flattened Array
     */
    public flat<U>(): Array<U> {
        return this.flat<U>(1)
    }

    /**
     * Applies flat and than map
     *
     * fn a function to apply
     *
     * @return new Array after map and than flat
     */
    // NOTE(ivan-tyulyandin): TBD, flatMap may be not subset, see ReadonlyArray
    public flatMap<U>(fn: (v: T, k: number, arr: Array<T>) => U): Array<U> {
        let mapped: Array<U> = this.map<U>(fn)
        return mapped.flat<U>()
    }

    // === methods common among all arrays ===
% require 'ostruct'
% ctx = OpenStruct.new
% $ctx = ctx
% ctx.this = 'this'
% ctx.this_arg = ''
% ctx.this_len_int = 'this.actualLength'
% ctx.this_array = 'escompat_array'
% ctx.array_len_int = Proc.new { |v| "#{v}.actualLength" }
% ctx.access_public = 'public'
% ctx.access_private = 'private'
% ctx.override = 'override'
% ctx.override_expected_here = '' # hide 'override' when it cannot be implemented
% ctx.get_unsafe = Proc.new { |t, i| "#{t}.$_get_unsafe(#{i})" }
% ctx.set_unsafe = Proc.new { |t, i, v| "#{t}.$_set_unsafe(#{i}, #{v})" }
% ctx.el_type = 'T'
% ctx.el_type_boxed = 'T'
% ctx.function_type = 'has_native'
% ctx.this_type = 'Array<T>'
% ctx.this_return_type = 'this'
% ctx.clone_this = 'new Array<T>(FROM_BUFFER, this.copyArray())'
% ctx.make_buffer = Proc.new { |l, elt|
%   "new NullishType[#{l}]"
% }
% ctx.make_fixed_array = 'FixedArray<NullishType>'
% ctx.from_buffer = Proc.new { |b, elt|
%   elt ||= ctx.el_type
%   "new Array<#{elt}>(FROM_BUFFER, #{b})"
% }
% ctx.array_of_type = Proc.new { |t| "Array<#{t}>" }
% ctx.this_call = Proc.new { |f| "this.#{f}(" }
% ctx.arr_method_call = Proc.new { |t, f, *args| "#{t}.#{f}(#{args.join(', ')})" }
% ctx.this_generic = ''
% ctx.this_generic_one = ''
% ctx.this_iterator_generic = '<T>'
% template = ERB.new(File.read("Array_common.erb"), trim_mode: '%', eoutvar: '_sub01')
<%= template.result(ctx.instance_eval { binding }).gsub(/^/, '    ') %>
% template = ERB.new(File.read("Array_map.erb"), trim_mode: '%', eoutvar: '_sub02')
% ctx.mapped_type = 'U'
% ctx.map_generic = '<U>'
<%= template.result(ctx.instance_eval { binding }).gsub(/^/, '    ') %>
}
% template = ERB.new(File.read("Array_common_top_scope.erb"), trim_mode: '%', eoutvar: '_sub03')
<%= template.result(ctx.instance_eval { binding }).rstrip %>
